iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
自我挑戰組

30 天 vueuse 原始碼閱讀與實作系列 第 27

[Day 27] useInfiniteScroll - unit test

  • 分享至 

  • xImage
  •  

useInfiniteScroll 官方 Demo:https://vueuse.org/core/useInfiniteScroll/#useinfinitescroll

單元測試結構 & helper function

import { flushPromises } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useInfiniteScroll } from '@/compositions/useInfiniteScroll'
import { useElementVisibility } from '@/compositions/useElementVisibility'

vi.mock('@/compositions/useElementVisibility')
describe('useInfiniteScroll', () => {

    // 測試案例放這邊
    
    function givenMockElement({
        scrollHeight = 0,
    } = {}) {
        const mockElement = document.createElement('div')
        Object.defineProperty(mockElement, 'scrollHeight', {
          value: scrollHeight,
        })
        return mockElement
    }

    function givenElementVisibilityRefMock(defaultValue) {
        const mockVisibilityRef = ref(defaultValue)

        // TS
        // vi.mocked(useElementVisibility).mockReturnValue(mockVisibilityRef)

        // JS
        useElementVisibility.mockReturnValue(mockVisibilityRef)

        return mockVisibilityRef
    }
})

先來看看 givenMockElement 這個 helper function,主要在測試案例中用來取得可以客製 scrollHeight 的 mockElement。這邊用到 Object.defineProperty 主要是因為 scrollHeight 是一個 read-only 的屬性。

接著來看 givenElementVisibilityRefMock 這個 helper function,主要在測試案例中 mock useElementVisibility 的回傳值,來測試 scroll container 出現或沒出現在可視範圍的情境。
因為要使用 mockReturnValue 所以開頭有 mock @/compositions/useElementVisibility 這個模組。

測試案例

接下來的測試案例都會放在 // 測試案例放這邊 這個註解的層級。

當 scroll container 有在可視範圍中,測試 loadMore 是否有執行

it.each([
    [ref(givenMockElement())],
    [givenMockElement()],
    [document],
    [window],
  ])('should calls the loadMore handler, when element is visible', (target) => {
    const mockHandler = vi.fn()
    givenElementVisibilityRefMock(true)

    useInfiniteScroll(target, mockHandler)

    expect(mockHandler).toHaveBeenCalledTimes(1)
})

這個案例用到 it.each 來處理多種參數類型的測試,target 分別會拿到 ref(givenMockElement())givenMockElement()documentwindow

useInfiniteScroll 接受的第二個參數是 loadMore function,這邊傳入一個 mock function mockHandler

另外有透過 givenElementVisibilityRefMock(true) 來 mock useElementVisibility 的回傳值 isElementVisible 為 true,根據昨天看到的 useInfiniteScroll 原始碼,isElementVisible 為 true 是 loadMore function 被執行的其中一個條件。

當 scroll container 從不在可視範圍,變成進到可視範圍中,測試 loadMore 是否有執行

it('should calls the loadMore handler, when element visibility state form hidden to visible', async () => {
    const mockHandler = vi.fn()
    const mockElement = givenMockElement()
    const visibilityRefMock = givenElementVisibilityRefMock(false)

    useInfiniteScroll(mockElement, mockHandler)

    expect(mockHandler).not.toHaveBeenCalled()

    visibilityRefMock.value = true
    await flushPromises()

    expect(mockHandler).toHaveBeenCalledTimes(1)
})

這個案例主要也是透過 mock useElementVisibility 的回傳值 isElementVisible 來做測試,一開始 isElementVisible 為 false 時,要驗證 mockHandler 不能被觸發。後來透過 visibilityRefMock.value = true,把 isElementVisible 設定為 true,useInfiniteScroll 內部的 watcher 會偵測到這個變動,進而執行 mockHandler

await flushPromises() 的用途是什麼呢?記得昨天提到 useInfiniteScroll 內部有使用到 Promise.all 來處理核心邏輯,使用 await flushPromises() 會讓當前還在 pending 的 Promise 立即完成,也會等 DOM 更新成最新狀態,才繼續後續的驗證。

vue-test-utils flushPromises 文件:https://test-utils.vuejs.org/api/#flushPromises

user scroll 的時候,驗證是否執行 loadMore

it('should call the loadMore handler, when user scrolls', async () => {
    const mockElementScrollHeight = 100
    const mockHandler = vi.fn()
    const mockElement = givenMockElement({
      scrollHeight: mockElementScrollHeight,
    })
    givenElementVisibilityRefMock(true)

    useInfiniteScroll(mockElement, mockHandler)
    mockElement.scrollTop = mockElementScrollHeight
    mockElement.dispatchEvent(new Event('scroll'))
    await flushPromises()

    expect(mockHandler).toHaveBeenCalledTimes(1)
})

這個案例重點在 scroll 到目標位置,有沒有成功執行到 mockHandler function。

透過一開始提到的 givenMockElement helper function 把 scrollHeight 設定成 100, mock useElementVisibility 的回傳值 isElementVisible 為 true,但這個 isElementVisible 為 true 並不會觸發 mockHandler,因為除了 isElementVisible 為 true 這個條件以外;還有另外一個條件是必須滾動到 scroll container 底部,目前還沒 scroll 所以這個條件不會成立。

接著我們把 mockElement.scrollTop 設定成 100,並使用 dispatchEvent 觸發 scroll,剛好 scroll 到最底部,最後驗證 mockHandler 有執行過一次。

GitHub:https://github.com/RhinoLee/30days_vue/pull/26/files


useInfiniteScroll API 就到今天告一段落啦~
剩餘的天數應該會來看 vueuse 是怎麼透過 VitePress 來生成官方文件的。


上一篇
[Day 26] useInfiniteScroll
下一篇
[Day 28] 使用 VitePress 生成 VueUse 文件頁面 - Part 1
系列文
30 天 vueuse 原始碼閱讀與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言